서비스 - 가상 IP 매커니즘
개요
이 문서는 서비스의 핵심 작동 원리를 담고자 분리된 문서이다.
서비스 문서에 전부 담기에는 조금 하드하지 않나 싶기도 했고, 공식 문서[1]에서도 일단 분리를 하고 있기 때문에 나도 이 방향이 바람직하다고 생각한다.
다만 이 문서에서는 원리에 대한 간단한 내용을 담는 것을 핵심으로 하고, 세부 설정을 하는 것은 서비스 문서로 옮기려고 한다.
태반이 서비스 양식을 어떻게 작성할 것인가에 관한 부분이라 구태여 나누어 설명하는 것이 더 큰 혼란을 야기한다고 생각했다.
아직도 지금 문서는 진짜 개판이라고 생각한다
이 문서에는 구체적으로 다음의 두 가지 내용을 담는다.
- 관리자의 네트워크 설정이 노드에 동작하는 과정
- 서비스가 클러스터 ip를 부여받는 과정
서비스의 동작 원리를 설명하는 만큼, 관련한 기능을 제공하는 kube-proxy와도 밀접한 관련이 있다.
kube-proxy를 통한 가상 IP 프록싱
서비스가 만들어지면 어떤 ip를 부여 받게 되는데, 이것은 어떻게 부여받는가?
이 물음 이전에, 클러스터에서 서비스의 ip로 들어온 트래픽이 노드에 대한 정보가 없어도 원하는 엔드포인트로 가는지 알아보자.
프록시
서비스 문서에서 간략하게 설명했듯이, 실제로 모든 트래픽에 대한 설정을 해주는 것은 바로 kube-proxy(이하 프록시)이다.
모는 노드에 배치된 이 프록시는 세부 설정을 하지 않는 이상 동일하게 동작한다.
이 친구들은 kube-apiserver를 통해 서비스, 엔드포인트슬라이스 오브젝트를 감시한다.
그리고 오브젝트들에 변화가 생기면 이를 자신에 노드에 적용하는 일을 한다.
그렇다면 프록시는 어떤 설정을 노드에 가하는가?
그 방법은 바로 iptables에 있다.
모든 노드에는 패킷을 필터링해서 트래픽을 내부로 라우팅하거나 그대로 전달하는(forward) NetFilter가 존재한다.
이 넷필터를 조작하는 방법이 바로 iptables라는 것이고, 이것은 규칙 리스트를 지정하여 적용하는 식으로 동작한다.
프록시는 이렇게 규칙 리스트를 작성하여 실제로 노드로 들어온 트래픽이 원하는 곳으로 전달되도록 만드는 것이다.
참고로 iptables 말고 다른 프록시 모드를 사용하는 설정도 가능하다.
자세한 내용은 kube-proxy를 참조하자.
프록시 간단 예시
서비스가 만들어지면 임의의 가상 ip가 만들어지고, 엔드포인트슬라이스 컨트롤러가 이 서비스에 매칭되는 엔드포인트들을 묶어 관리한다.
새로운 서비스를 프록시가 관측하면 일련의 여러 iptables 규칙들을 만든다.
구체적으로는 서비스에서 엔포슬로 관리되는 각 엔드포인트로 프록시되는 규칙, 그리고 엔드포인트로의 트래픽이 실제 파드로 가도록 DNAT(가는 목적지 수정)하는 규칙을 관리한다.
그래서 궁극적으로 서비스의 가상 ip로의 트래픽은 엔드포인트로 흘러들어가고, 이후 백엔드까지 흘러가게 된다.
iptables 룰을 보기 쉽게 잘 설명한 듯..[2]
PREROUTING - KUBE-SERVICES - KUBE-SVC-... - KUBE-SEP-...
의 순서로 체인이 연결된다.
SVC가 각 서비스를 나타내며, 다음 단계에서 주석 뒷부분 설정을 통해(random probability) 랜덤 부하분산된다.
마지막 체인에서 DNAT되어 실제 파드의 ip로 이어지게 된다.
실제로 어떻게 규칙들을 쓰는가 보니까 진짜로 일일히 iptables 명령어를 만들어서 명령을 내리는 방식이다;[3]
왜 프록시를 사용하나?
프록시를 사용하는 이유는 무엇일까?
가령 DNS를 활용해 한 서비스의 도메인네임을 쿼리하면 파드의 ip를 바로 찔러주는 A레코드를 만들면 안되는 걸까?
dns 차원의 로드 밸런싱(round-robin name resolution) 방법도 존재하니, 서비스가 바라는 기능을 충족할 수 있을 지도 모른다.
그러나 dns를 사용하지 않는 데에는 몇 가지 이유가 있다.
- DNS는 TTL을 감안하지 않고 캐싱을 하는 관례가 오래 이어져왔다.
- 어떤 어플리케이션은 DNS를 한번 조회한 이후 무기한 캐싱한다.
- 이게 다 만족이 된다고 한들, 그래서 DNS의 캐싱 기한을 매우 짧게 한다고 하면 트래픽 상에서 어마무시한 부하가 걸리게 된다.
반면 kube-proxy는 커널 레벨의 룰을 수정한다.
그래서 패킷을 뜯어 어디로 가야 하는지 파악하고 이를 빠르게 라우팅하는데 효과적이다.
참고로 프록시 규칙을 맘대로 커스텀하려 했다가는 노드를 재부팅하지 않는 이상 수정되지 않을 수도 있다..
그래서 이 프록시를 직접 다루는 건 낮은 레벨에서의 조작 결과를 아는 관리자만이 해야 한다.
프록시 모드
기본 원리는 iptables로 설명했으나, kube-proxy가 어떤 툴을 활용하여 프록시 규칙을 설정할 것인가에 따라 몇 가지 타입을 세분화시킬 수 있다.
각각의 차이를 깊게 보기에는 내 수준이 조금 얕고, 이해를 증진시키기 위한 차원에서만 설명한다.
iptables
리눅스에서만 가능한 모드로, 커널의 넷필터 subsystem에 있는 iptables API를 이용한다.
기본적으로 각 엔드포인트는 무작위 파드와 매칭된다.
iptables 최적화
kube-proxy에 대해서 몇 가지 설정들을 해주는 방식으로 부하 최소화를 통한 최적화를 꾀할 수 있다.
iptables:
minSyncPeriod: 1s
syncPeriod: 30s
iptables에서는 모든 서비스와 모든 엔드포인트에 룰이 추가된다.
1000개의 서비스와 엔드포인트가 있다면? 룰을 변경하고 적용하는데 시간도 오래 걸릴 것이다.
이때 위의 설정들을 하는 것이 도움이 될 수 있다.
minSyncPeriod
는 재동기화를 하는 최소 기간을 지정한다.
이 값이 0이면 서비스가 생기거나 바뀔 때마다 프록시는 값을 반영한다.
짧게 많이 서비스를 수정하는 경우 부하가 걸리게 될 것이다.
또 100개의 파드를 관리하는 디플을 없앤다고 한다면, 이 기간이 조금 여유가 있으면 룰을 수정할 때 한꺼번에 수정하게 돼서 부하가 줄어든다.
그래서 오히려 부하가 줄어들어 결과가 빠르게 반영되게 될 수도 있다.
물론 이 값이 크면 동기화에 걸리는 텀이 길어지니 안 좋을 수 있다.
기본 값은 1초이며, 규모가 큰 클러스터에서는 조금 더 큰 값이 필요할 수도 있다.
프록시 메트릭 중 sync_proxy_rules_duration_seconds
라는 값의 평균이 1초보다 크다면 이 값을 늘리는 방향으로 효율화해보자.
syncPeriod
파라미터는 개별 서비스와 엔드포인트의 변경과 직접적으로는 관련 없는 동기화 작업을 제어한다.
구체적으로는 프록시와 관련 없는 외부 컴포넌트의 개입이 일어난 것을 얼마나 빠르게 감지하는가에 대한 것이다.
큰 클러스터에서는 한번씩 불필요한 작업을 정리하는 시간이 필요하긴 하다.
대체로는 이게 큰 영향을 끼치지는 않는데, 과거에는 아예 1시간으로 설정하는 케이스가 있었다고 한다.
지금은 추천되지 않는데 이게 오히려 기능성에 영향을 주기 때문이다.
ipvs
사진으로는.. 뭐가 다른지는 잘 모르겠다.
리눅스 노드에서만 가능한 방식으로, ipvs는 커널의 ipvs와 iptables api를 사용한다.
iptables 모드와 비슷하나, 커널 스페이스에서 동작하는 해시 테이블을 사용한다.
그래서 레이턴시가 조금 더 줄어들고, 프록시 규칙 동기화의 성능이 올라간다.
심지어 처리량에서도 조금 더 좋은 성능을 보인다고 한다.
트래픽을 밸런싱하는 다양한 옵션을 제공한다.
- rr - Round Robin
- 뒷단 서버에 공평하게 트래픽이 분배된다.
- wrr - Weighted Round Robin
- 가중치를 두고 트래픽을 분배한다.
- lc - Least Connection
- 적은 연결을 가진 서버에 트래픽이 간다.
- wlc - Weighted Least Connection
- 적은 연결을 가진 서버가 가중치를 더 가며, 이를 기준으로 분배된다.
- lblc - Locality based Least Connection
- 같은 ip 주소로부터 온 트래픽은 가능한 같은 서버로 연결한다.
- 그게 안 되면 적은 연결을 가지는 쪽으로 연결한다.
- lblcr - Locality Based Least Connection with Replication
- 같은 ip 주소로부터 온 트래픽은 가능한 같은 서버로 연결한다.
- 모든 서버가 과부하가 걸리면, 그나마 적은 곳을 타겟 집합으로 지정한다.
- 일정 시간 동안 집합에 변경사항이 없다면 높은 수준의 복제를 피하기 위해 부하가 큰 서버가 세트에서 제거된다.
- 이게 무슨 말인지 잘 모르겠다.
- sh - Source Hashing
- 들어온 ip 주소 기반으로 해시 테이블 기반으로 분배된다.
- dh - Destination Hashing
- 목적지 ip 주소 기반으로 해시 테이블 기반으로 분배된다.
- sed - Shortest Expected Delay
- 가장 적게 지연될 것 같은 곳으로 분배한다.
(서버에 연결된 커넥션 수 + 1) / 고정된 서버 가중치
로 예상 지연 시간을 구한다.
- nq - Never Queue
- 빠른 서버를 기다리는 대신, 유휴 서버로 일단 트래픽을 보낸다.
- 모든 서버가 바쁘다면 sed랑 똑같이 작동한다.
- mh - Maglev Hashing
- 마글레브 해시 알고리즘을 사용한다.
- 이게 뭔지는 좀 알아봐야 할 듯..
ipvs를 쓰려면 노드에서 먼저 ipvs가 가능하게 해야 한다.
이게 안 된 채로 프록시를 가동시키면 에러가 난다.
nftables
5.13 커널을 가진 리눅스 노드에서만 가능하며, nftables api를 사용한다.
iptables의 후속자로, 조금 더 좋은 성능과 유연성을 가지고 있다.
엔드포인트를 바꾸는 등 작업에 더 효율적이고, 커널 단의 패킷 처리도 더 빠르다고 한다.
그러나 수만 개의 서비스가 있는 클러스터 정도는 돼야 눈에 띈다고 한다.
Kubernetes v1.32 - Penelope에서도 아직 새로운 모드에 속하는 정도라, 클러스터에서 사용하는 CNI 플러그인이 이를 지원하는지는 확인이 꼭 필요하다.
iptables로부터 마이그레이션
마이그레이션을 할 때 조금 알아야 할 사항들이 있다.
- 노드포트 인터페이스
- 원래 노드포트 서비스는 모든 로컬 ip에서 접근이 가능하나, 여기에서는
--nodeport-addresses primary
이다. - 즉, 기본 ip 주소일 때만 접근이 가능하다.
--nodeport-addresses 0.0.0.0/0
으로 설정하면 똑같이 작동한다.
- 원래 노드포트 서비스는 모든 로컬 ip에서 접근이 가능하나, 여기에서는
127.0.0.1
인 노드포트 서비스- 원래는
--nodeport-addresses
의 범위가127.0.0.1
를 포함하면, localhost를 통해 접근이 가능하지만 여기에선 그렇지 않다. - 만약 문제가 발생하면
iptables_localhost_nodeports_accepted_packets_total
메트릭을 확인해 이런 접근이 있는지 확인하라.
- 원래는
- 노드포트의 방화벽 상호작용
- 원래 프록시는 방화벽을 잘 통과한다.
- 원하는 포트에 인바운드 트래픽 규칙을 추가해 방화벽에 안 걸리게 만드는 것이다.
- nftables를 쓸 때는 노드포트 범위의 값을 잘 허용하는지, 직접 확인하여 설정해야 한다.
- Conntrack 버그 해결책
- 6.1 버전 이전 커널은 오래 이어지는 tcp 연결에 대해 "Connection reset by Peer" 에러를 띄운다.
- iptables는 이 에러를 해결하는 조치를 취하나 이것의 문제점이 추후 발견됐다.
- 그래서 nftables는 이러한 조치를 취하지 않는다.
- 만약 프록시에
iptables_ct_state_invalid_dropped_packets_total
메트릭이 발견된다면,--conntrack-tcp-be-liberal
옵션을 추가하라.
kernelspace
윈도우에서 사용되는 모드인데, 윈도우 네트워크를 잘 몰라 매우 간략하게..
kube-proxy는 윈도우 vSwitch의 확장인 VFP(Virtual Filtering Platform)를 사용하게 된다.
노드 레벨의 가상 네트워크에 대해서 작업을 진행하고, DNAT를 해준다.
프록시에서 다른 노드의 파드로 가는 규칙을 쓰게 된다면, 윈도우의 HNS(Host Networking Service)가 응답패킷들이 잘 돌아올 수 있도록 보장해준다고 한다.
가상 IP가 서비스에 할당되는 원리
파드는 각 노드에서 할당되는 ip를 기본적으로 받는다.
(CNI마다 조금씩 설정이 다를 수도 있는데, Calico의 경우는 기본적으로 자체 ipam으로 대역을 설정해준다.)
그러나 서비스의 ip는 클러스터 전역에서 활용될 수 있는 범위를 토대로 ip를 부여받는다.
이것은 어떤 노드에 특정되지 않고 전역적으로 사용될 수 있는 가상 IP(Virtual IP)이다.
그래서 서비스의 ip는 단일 노드에서 값을 가진 채로 답을 내리는 것이 아니다.
서비스 ip에 대해 모든 노드의 프록시는 패킷 처리 로직을 사용하여, 투명하게(클라는 모르게) 리디렉트되도록 한다.
클라이언트가 가상 ip로 트래픽을 날려 어떤 노드에든 연결이 되면, 그들의 트래픽은 적절한 엔드포인트로 전송되고 문제가 발생하지 않는 것이다.
이런 원리 덕에 모든 노드에서 서비스의 ip를 자유롭게 사용할 수 있는 것이다.
ip 충돌 회피
쿠버네티스의 철학 중 하나는 관리자의 잘못이 아닌 이유로 작업 실패가 일어나지 않는 것이다.
그래서 서비스 ip를 지정할 때는 누군가의 선택과 충돌할 수 있는 선택이 허용되지 않는다. (이거 너무 번역투인데)
그래서 ip는 kube-apiserver에 명시된 범위 내에서 자동적으로 할당된다.
api서버에서 service-cluster-ip-range
cidr 범위를 사용해 ip를 할당하며, 기본적으로 충돌이 일어나지 않게 한다.
구체적으로는 고유한 ip 주소를 얻는 것을 보장하기 위해 내부적인 할당자가 원자적으로 Etcd에 전역 할당 맵을 만들어둔다.
이 맵을 통해 정확하게 할당 가능한 ip 주소를 트래킹하며 ip를 할당해주는 것이다.
맵은 백그라운드 컨트롤러가 책임을 지는데, 이것은 관리자의 유효하지 않은 할당을 막고, 안 쓰이는 서비스에 대한 주소를 청소하는 일까지 해준다!
ip 범위 커스텀
근데.. Kubernetes v1.31 - Elli에서 MultiCIDRServiceAllocator
피처 게이트를 활성화하면 이걸 또 커스텀 가능하다.
IPAddress, ServiceCIDR 오브젝트를 내부의 전역 할당 맵 대신 사용하게 되는 것이다.
이때 IPAddress 오브젝트가 각 서비스의 클러스터 ip에 할당될 것이다.
그러나 Kubernetes v1.32 - Penelope에서조차 이 기능을 활성화하기 이전에서의 자동 마이그레이션은 제공하지 않으니 조심하자.
Kubernetes v1.33 - Octarine부터 기본적으로 사용이 가능해졌다!
이런 방식은 내 맘대로 서비스의 ip 범위를 지정할 수 있단 장점이 있다.
예를 들어 서비스를 위한 IP 대역이 부족해진다면 이 리소스를 만들어서 클러스터 재기동 없이 IP 대역을 확장하거나 추가할 수 있게 되는 것이다.
ipv4에서는 제한이 없고, ipv6에서도 /108까지이던 넷마스크를 /64나 그 아래까지도 지정할 수 있게 된다.
또한 유저의 커스텀으로 ip 주소를 지정할 수 있다는 장점도 있다(장점 맞냐).
Gateway API에서는 이걸 활용해 내부 네트워킹 능력을 확장한다고 한다.
직접 해보진 않을 것 같아서 사진만 남겨둔다.
자세한 내용은 문서 참고![4]
ip 밴드를 통한 정적 할당
자동이 아닌, 특정 ip를 정적으로 할당해야 하는 케이스가 있을 수 있다.[5]
대표적인 예시가 dns 서버인데, 주어진 ip 대역 내에서 관례적으로 dns 서버는 10번째 ip를 할당한다.
흔한 케이스는 아니겠지만, 이런 식으로 ip를 직접 할당할 때 ip 충돌 가능성은 더 커지기 마련이다.
이에 대해 쿠버네티스는 기본적으로 ip를 두 밴드로 나눈 후 정적으로 할당할 수 있는 범위를 제공해준다.
min(max(16, {cidrSize, 즉 확보된 호스트 주소 개수} / 16), 256)
공식은 이렇게 되는데, 간단하게 말하자면 클러스터 ip 범위에 따라 16~256개 정도의 ip를 수동으로 자유롭게 할당할 수 있다.
만약 서비스 대역 범위가 10.96.0.0/24
라면?
cidrSize는 256일 것이고 위 공식에 따르면 min(max(16, 256/16), 256)으로 16의 값이 나온다.
그래서 16개의 ip가 충돌을 회피하는 정적 할당이 보장된다.
그럼 10.96.0.0/16
의 범위 내에서는?
cidrSize는 65536, min(max(16, 65536/16), 256)으로 256이 나오게 된다.
그래서 10.96.0.1 ~ 10.96.1.0
의 범위를 맘대로 쓸 수 있다!
관련 문서
이름 | noteType | created |
---|---|---|
Service | knowledge | 2024-12-29 |
서비스 - 가상 IP 매커니즘 | knowledge | 2025-01-02 |
가상 IP 매커니즘 | knowledge | 2025-05-04 |
EndpointSlice | knowledge | 2025-02-16 |
LoxiLB | knowledge | 2025-01-07 |
AWS Load Balancer Controller | knowledge | 2025-02-12 |
2W - ALB Controller, External DNS | published | 2025-02-15 |
E-레디네스 프로브와 레디네스 게이트 | topic/explain | 2024-08-15 |
I-EndpointSlice 분산 로직 분석 | topic/idea | 2025-01-03 |
S-flannel dns 질의 실패 | topic/shooting | 2024-09-11 |
T-스테이트풀셋과 연결되는 헤드리스 서비스에 관한 실험 | topic/temp | 2024-12-27 |
T-LoxiLB vs MetalLB | topic/temp | 2025-01-06 |
참고
https://kubernetes.io/docs/reference/networking/virtual-ips/ ↩︎
https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go ↩︎
https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/ ↩︎
https://kubernetes.io/docs/concepts/services-networking/cluster-ip-allocation/ ↩︎